package storage

import (
	"bytes"
	"errors"
	"fmt"
	"gmfs/core/logger"
	"gmfs/core/tools/pic"
	"gmfs/core/tools/pic/nude"
	"gmfs/core/tools/pic/resize"
	"gmfs/core/tools/util"
	"gmfs/core/web/ink"
	"gmfs/core/web/model"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
	"image"
	"image/color/palette"
	"image/draw"
	"image/gif"
	"image/png"
	"io"
	"os"
	"time"
)

const db_name = "gmfs"

/**
 * image gridfs
 */
func imageGridfs(session *mgo.Session) *mgo.GridFS {
	return session.DB(db_name).GridFS("img")
}

/**
 * file gridfs
 */
func fileGridfs(session *mgo.Session) *mgo.GridFS {
	return session.DB(db_name).GridFS("file")
}

/**
 * Collection
 */
func getCollection(session *mgo.Session, name string) *mgo.Collection {
	return session.DB(db_name).C(name)
}

/**
 * 文件资源
 */
func getRes(session *mgo.Session) *mgo.Collection {
	return getCollection(session, "res")
}

/**
 * 文件信息
 */
func getInfo(session *mgo.Session) *mgo.Collection {
	return getCollection(session, "info")
}

/**
 * 文件黑名单
 */
func getBlacklist(session *mgo.Session) *mgo.Collection {
	return getCollection(session, "blacklist")
}

/**
 * 转换静态图片
 */
func ResizeImage(ctx *ink.Context, gf *mgo.GridFile, b bool, w, h uint) *bytes.Buffer {
	var buf = new(bytes.Buffer)
	var use = useWatermark(ctx, w, h)
	img, name, _ := pic.DecodeImage(gf)
	//true 处理图片，false 不处理原样输出
	if b {
		dst := resize.Resize(w, h, img, resize.Lanczos3)
		//判断是否添加水印
		if use {
			m, err := addWatermark(ctx, dst)
			if err != nil {
				logger.Errorln(err)
			} else {
				//返回水印图片
				pic.Encode(buf, m, name)
				return buf
			}
		}

		pic.Encode(buf, dst, name)
	} else {
		//判断是否添加水印
		if use {
			m, err := addWatermark(ctx, img)
			if err != nil {
				logger.Errorln(err)
			} else {
				//返回水印图片
				pic.Encode(buf, m, name)
				return buf
			}
		}

		pic.Encode(buf, img, name)
	}

	return buf
}

/**
 * 转换GIF图片
 */
func ResizeGif(ctx *ink.Context, r io.Reader, rs bool, width, hight uint) *bytes.Buffer {
	var buf = new(bytes.Buffer)
	var use = useWatermark(ctx, width, hight)

	// Decode the original gif.
	im, err := gif.DecodeAll(r)
	if err != nil {
		return nil
	}

	// Create a new RGBA image to hold the incremental frames.
	firstFrame := im.Image[0].Bounds()
	b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
	img := image.NewRGBA(b)

	// Resize each frame.
	for index, frame := range im.Image {
		bounds := frame.Bounds()
		draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
		//是否处理图片
		if rs {
			//是否添加水印
			if use {
				wmimg, _ := addWatermark(ctx, img)
				im.Image[index] = ImageToPaletted(ProcessImage(wmimg, width, hight))
			} else {
				im.Image[index] = ImageToPaletted(ProcessImage(img, width, hight))
			}
		} else {
			//是否添加水印
			if use {
				wmimg, _ := addWatermark(ctx, img)
				im.Image[index] = ImageToPaletted(wmimg)
			} else {
				im.Image[index] = ImageToPaletted(img)
			}
		}
	}

	gif.EncodeAll(buf, im)
	return buf
}

/**
 * 执行图片剪切
 */
func ProcessImage(img image.Image, width, hight uint) image.Image {
	return resize.Resize(width, hight, img, resize.NearestNeighbor)
}

func ImageToPaletted(img image.Image) *image.Paletted {
	b := img.Bounds()
	pm := image.NewPaletted(b, palette.Plan9)
	draw.FloydSteinberg.Draw(pm, b, img, image.ZP)
	return pm
}

/**
 * 添加水印
 */
func addWatermark(ctx *ink.Context, img image.Image) (*image.NRGBA, error) {
	var wp = ctx.App().Config().String("img.watermark_path")
	wmb, _ := os.Open(wp)
	watermark, err := png.Decode(wmb)
	if err != nil {
		return nil, err
	}
	defer wmb.Close()

	//把水印写到右下角，并向0坐标各偏移10个像素
	offset := image.Pt(img.Bounds().Dx()-watermark.Bounds().Dx()-10, img.Bounds().Dy()-watermark.Bounds().Dy()-10)
	b := img.Bounds()
	m := image.NewNRGBA(b)

	draw.Draw(m, b, img, image.ZP, draw.Src)
	draw.Draw(m, watermark.Bounds().Add(offset), watermark, image.ZP, draw.Over)
	return m, nil
}

/**
 * 判断是否需要加水印
 */
func useWatermark(ctx *ink.Context, w, h uint) bool {
	var rlt = false
	var use = ctx.App().Config().Bool("img.watermark_use")
	if !use {
		//全局设置不开启水印，请求可通过该参数打开水印
		watermark := ctx.Input()["watermark"]
		if watermark == "true" {
			use = true
		}
	}
	if use {
		width := ctx.App().Config().Int("img.watermark_width")
		height := ctx.App().Config().Int("img.watermark_height")
		//w,h 为0 原图加水印，w不为0 h为 0 等比缩放
		if w >= uint(width) || w == 0 {
			if h == 0 || h >= uint(height) {
				rlt = true
			}
		}
	}
	return rlt
}

/**
 * 根据 md5,length
 * 查询文件是否存在黑名单中
 */
func ExistBlacklist(session *mgo.Session, fileMd5 string, fileSize int) (*model.Blacklist, error) {
	var result *model.Blacklist
	err := getBlacklist(session).Find(bson.M{"md5": fileMd5, "size": fileSize}).One(&result)
	if err != nil {
		return nil, err
	}
	logger.Debugf(" search ImageBlacklistExist : %v", result)
	return result, nil
}

/**
 * 根据 id
 * 查询文件
 */
func FindInfoById(session *mgo.Session, id string) (*model.Info, *mgo.GridFile, error) {
	isId := bson.IsObjectIdHex(id)
	if !isId {
		return nil, nil, errors.New("id error")
	}
	inf, err := queryInfoById(session, id)
	if err != nil {
		return nil, nil, err
	}
	gf, err := queryGridFile(session, inf.GetFileType(), bson.M{"_id": bson.ObjectIdHex(inf.GetFid())})
	return inf, gf, err
}

/**
 * 根据 id
 * 查询文件信息
 */
func queryInfoById(session *mgo.Session, id string) (*model.Info, error) {
	var inf *model.Info
	err := getInfo(session).FindId(bson.ObjectIdHex(id)).One(&inf)
	if err != nil {
		return nil, err
	}
	return inf, nil
}

/**
 * 根据 Fid
 * 查询文件资源
 */
func queryResByFid(session *mgo.Session, fid string) (*model.Res, error) {
	var res *model.Res
	err := getRes(session).Find(bson.M{"fid": fid}).One(&res)
	if err != nil {
		return nil, err
	}
	return res, nil
}

/**
 * 查询图片
 */
func queryGridFile(session *mgo.Session, fileType int, query bson.M) (*mgo.GridFile, error) {
	var fp *mgo.GridFile
	var gfs *mgo.GridFS

	if fileType == 0 {
		//0、图片文件
		gfs = imageGridfs(session)
	} else {
		//1、二进制文件
		gfs = fileGridfs(session)
	}

	iter := gfs.Find(query).Iter()
	gfs.OpenNext(iter, &fp)

	if fp == nil {
		return fp, fmt.Errorf("no image found for %s", query)
	}

	return fp, nil
}

/**
 * 删除 gridfs 图片文件
 */
func removeImgGridfs(session *mgo.Session, id string) error {
	return imageGridfs(session).RemoveId(bson.ObjectIdHex(id))
}

/**
 * 删除图片资源文件
 */
func removeImgRes(session *mgo.Session, resId bson.ObjectId, fid string) error {
	err := getRes(session).RemoveId(resId)
	if err != nil {
		return err
	}
	return imageGridfs(session).RemoveId(bson.ObjectIdHex(fid))
}

/**
 * 根据 md5,length
 * 查询文件是否存在
 */
func resExist(session *mgo.Session, fileMd5 string, fileSize int) (*model.Res, error) {
	var result *model.Res
	err := getRes(session).Find(bson.M{"md5": fileMd5, "size": fileSize}).One(&result)
	if err != nil {
		return nil, err
	}
	return result, nil
}

/**
 * 秒传根据 md5,length
 * 查询文件信息
 */
func SearchExist(session *mgo.Session, fileMd5 string, fileSize int) (*model.Info, error) {
	res, err := resExist(session, fileMd5, fileSize)
	if err != nil {
		logger.Errorf(" SearchExist : %v", err)
		return nil, err
	}
	inf, err := saveInfo(session, res)
	if err != nil {
		logger.Errorf(" SearchExist saveInfo: %v", err)
		return nil, err
	}
	return inf, nil
}

/**
 * 保存文件资源
 */
func saveRes(ctx *ink.Context, session *mgo.Session, res *model.Res) (string, string, error) {
	id := bson.NewObjectId()
	err := getRes(session).Insert(&model.Res{id, res.GetFid(), res.GetMd5(), res.GetSize(),
		res.GetRefnum(), res.GetNude(), res.GetOnline(), res.GetFileType(),
		res.GetFileSuffix(), res.GetContentType(), res.GetUploadDate()})
	if err != nil {
		logger.Errorf(" saveRes error: ", err)
		return "", "", err
	}

	//重置图片处理
	if res.GetFileType() == 0 {
		rif, _ := resetImgInfo(ctx, session, res)
		if rif != nil {
			return rif.GetFid(), rif.GetRdm(), nil
		}
	}

	//正常上传
	inf, err := saveInfo(session, res)
	if err != nil {
		logger.Errorf(" saveRes saveInfo error: ", err)
		return "", "", err
	}
	return inf.GetIdToString(), inf.GetRdm(), nil
}

/**
 * 替换图片文件
 * 参数： resetId 重置ID , resetRdm 重置随机数
 */
func resetImgInfo(ctx *ink.Context, session *mgo.Session, res *model.Res) (*model.Info, error) {
	resetId := ctx.Input()["resetId"]
	resetRdm := ctx.Input()["resetRdm"]
	if resetId != "" && resetRdm != "" {
		//查询当前关联的文件资源
		inf, err := queryInfoById(session, resetId)
		if err != nil {
			return nil, err
		}
		rf, err := queryResByFid(session, inf.GetFid())
		if err != nil {
			return nil, err
		}

		//文件资源是否被引用，是 -1 否 删除
		if rf.GetRefnum() >= 1 {
			updateResRefnum(session, rf, false)
		} else {
			removeImgRes(session, rf.GetId(), rf.GetFid())
		}

		//更新关联资源信息
		i := model.NewInfoByRes(res)
		err = getInfo(session).Update(bson.M{"_id": bson.ObjectIdHex(resetId), "rdm": resetRdm},
			bson.M{"$set": bson.M{"fid": i.GetFid(), "nude": i.GetNude(), "online": i.GetOnline(),
				"filetype": i.GetFileType(), "filesuffix": i.GetFileSuffix(), "contenttype": i.GetContentType(),
				"uploaddate": i.GetUploadDate()}})
		if err != nil {
			logger.Errorf(" resetImgInfo error: ", err)
			return nil, err
		}

		//ID保存不变
		i.SetFid(resetId)
		return i, nil
	}
	return nil, nil
}

/**
 * 保存文件信息
 */
func saveInfo(session *mgo.Session, res *model.Res) (*model.Info, error) {
	i := model.NewInfoByRes(res)
	err := getInfo(session).Insert(&model.Info{i.GetId(), i.GetFid(), i.GetRdm(),
		i.GetNude(), i.GetOnline(), i.GetFileType(), i.GetFileSuffix(),
		i.GetContentType(), i.GetUploadDate()})
	if err != nil {
		logger.Errorf(" saveImginfo error: ", err)
		return nil, err
	}
	return i, nil
}

/**
 * 更新文件资源，引用次数
 * 当上传的 md5 size 值一样时更新该参数，生成新的引用信息
 */
func updateResRefnum(session *mgo.Session, res *model.Res, add bool) (string, string, error) {
	var refnum = res.GetRefnum()
	if add {
		refnum = refnum + 1
	} else {
		refnum = refnum - 1
	}
	err := getRes(session).UpdateId(res.GetId(), bson.M{"$set": bson.M{"refnum": refnum}})
	if err != nil {
		logger.Errorf(" updateResRefnum error: ", err)
		return "", "", err
	}
	if !add {
		//重置或删除图片，减去引用次数，直接返回
		return "", "", nil
	}
	i, err := saveInfo(session, res)
	if err != nil {
		logger.Errorf(" updateResRefnum saveInfo error: ", err)
		return "", "", err
	}
	return i.GetIdToString(), i.GetRdm(), nil
}

/**
 * 文件转正
 */
func UpdateInfoOnline(session *mgo.Session, id, rdm string) error {
	err := getInfo(session).Update(bson.M{"_id": bson.ObjectIdHex(id), "rdm": rdm}, bson.M{"$set": bson.M{"online": true}})
	if err != nil {
		return err
	}
	inf, err := queryInfoById(session, id)
	if err != nil {
		return err
	}
	return getRes(session).Update(bson.M{"fid": inf.GetFid()}, bson.M{"$set": bson.M{"online": true}})
}

/**
 * 保存图片
 */
func SaveImage(ctx *ink.Context, session *mgo.Session, data []byte) (string, string, bool, error) {
	var uploadImg image.Image

	//之前是否上传
	res := model.NewRes(data)
	rf, err := resExist(session, res.GetMd5(), res.GetSize())
	if err != nil && err.Error() != "not found" {
		return "", "", false, err
	}

	//是否属于黑名单
	ibl, err := ExistBlacklist(session, res.GetMd5(), res.GetSize())
	if ibl != nil {
		return "photo in blacklist", "", true, err
	}

	//上传图片
	img, suffix, err := pic.DecodeImage(bytes.NewBuffer(data))
	if err != nil {
		return "", "", false, err
	}

	//鉴黄
	isNude, err := nude.IsImageNude(img)
	res.SetNude(isNude)
	nudeUpload := ctx.App().Config().Bool("img.nude_upload")
	if !nudeUpload {
		//不允许上传
		if err != nil {
			return "", "", false, err
		}
		if isNude {
			return "photo may be naked.", "", true, nil
		}
	} else {
		//用户要求涉黄验证
		nude := ctx.Input()["nude"]
		if nude == "true" {
			//不允许上传
			if err != nil {
				return "", "", false, err
			}
			if isNude {
				return "photo may be naked.", "", true, nil
			}
		}
	}

	//已存在，引用+1 生成新的引用信息
	if rf != nil {
		fid, rdm, err := updateResRefnum(session, rf, true)
		return fid, rdm, rf.GetNude(), err
	}

	//图片处理
	paramIc := ctx.Input()["ic"]
	if bson.IsObjectIdHex(paramIc) {
		//暂时不处理该逻辑
		//		gift := giftImageClassify(session, paramIc)
		//		if gift != nil {
		//			dst := image.NewRGBA(gift.Bounds(img.Bounds()))
		//			gift.Draw(dst, img)
		//			//设置 md5 size
		//			var buf = new(bytes.Buffer)
		//			pic.Encode(buf, dst, suffix)
		//			ii.SetMd5(util.ByteMD5(buf.Bytes()))
		//			ii.SetSize(len(buf.Bytes()))
		//			uploadImg = dst
		//		}
		uploadImg = img
	} else {
		uploadImg = img
	}

	//上传 gridfs
	tgf, err := imageGridfs(session).Create(util.GenerateRandomFilename(true, suffix))
	defer tgf.Close()
	if err != nil {
		return "", "", false, err
	}

	var fid = bson.NewObjectId()
	tgf.SetId(fid)
	tgf.SetContentType(fmt.Sprintf("image/%s", suffix))

	//add Meta
	width := uploadImg.Bounds().Dx()
	height := uploadImg.Bounds().Dy()
	metadata := bson.M{"width": width, "height": height}
	tgf.SetMeta(metadata)

	if suffix == "gif" {
		_, err = io.Copy(tgf, bytes.NewReader(data))
		if err != nil {
			return "", "", false, err
		}
	} else {
		var buf = new(bytes.Buffer)
		pic.Encode(buf, uploadImg, suffix)
		_, err = io.Copy(tgf, bytes.NewReader(buf.Bytes()))
		if err != nil {
			return "", "", false, err
		}
	}

	//save res
	res.SetOnline(fileOnline(ctx))
	res.SetFid(fid.Hex())
	res.SetFileType(0)
	res.SetFileSuffix(fmt.Sprintf(".%s", suffix))
	res.SetContentType(tgf.ContentType())
	res.SetUploadDate(time.Now())
	fileId, rdm, err := saveRes(ctx, session, res)
	if err != nil {
		return "", "", false, err
	}

	return fileId, rdm, isNude, nil
}

/**
 * 保存文件
 */
func SaveFile(ctx *ink.Context, session *mgo.Session, data []byte, fileSuffix, filetype string) (string, string, error) {
	//之前是否上传
	res := model.NewRes(data)
	rf, err := resExist(session, res.GetMd5(), res.GetSize())
	if err != nil && err.Error() != "not found" {
		return "", "", err
	}

	//是否属于黑名单
	ibl, err := ExistBlacklist(session, res.GetMd5(), res.GetSize())
	if ibl != nil {
		return "file in blacklist", "", err
	}

	//已存在，引用+1 生成新的引用信息
	if rf != nil {
		return updateResRefnum(session, rf, true)
	}

	//上传 gridfs
	tgf, err := fileGridfs(session).Create(util.GenerateRandomFilename(false, fileSuffix))
	defer tgf.Close()
	if err != nil {
		return "", "", err
	}

	var fid = bson.NewObjectId()
	tgf.SetId(fid)
	tgf.SetContentType(filetype)
	_, err = io.Copy(tgf, bytes.NewReader(data))
	if err != nil {
		return "", "", err
	}

	//save info
	res.SetFid(fid.Hex())
	res.SetNude(false)
	res.SetOnline(fileOnline(ctx))
	res.SetMd5(util.ByteMD5(data))
	res.SetSize(len(data))
	res.SetFileType(1)
	res.SetFileSuffix(fileSuffix)
	res.SetContentType(tgf.ContentType())
	res.SetUploadDate(time.Now())
	fileId, rdm, err := saveRes(ctx, session, res)
	if err != nil {
		return "", "", err
	}

	return fileId, rdm, nil
}

/**
 * 全局允许上线 online 为 false 不允许上线才生效
 */
func fileOnline(ctx *ink.Context) bool {
	var ol = true
	fo := ctx.App().Config().Bool("app.file_online")
	if fo {
		online := ctx.Input()["online"]
		if online == "false" {
			ol = false
		}
	} else {
		ol = false
	}
	return ol
}
